/**
 * 
 */
package com.android.zing;

import java.net.URLDecoder;
import java.util.Date;
import java.util.HashMap;

import org.json.JSONException;
import org.json.JSONObject;

import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences.Editor;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;

import com.android.zing.dialog.ZME_Dialog;
import com.android.zing.dialog.ZME_DialogError;

/**
 * @author trangdoan
 * 
 */
public class ZME_Base {

	public static final String GRAPHAPI_VERSION = "1.0";
	public static final String CLIENT_GRAPHAPI_VERSION = "android-1.0";

	public static final String REDIRECT_URI = "zmConnect://success";
	public static final String CANCEL_URI = "zmConnect://cancel";
	public static final String TOKEN = "access_token";
	public static final String EXPIRES = "expires";
	public static final String AUTH_CODE = "code";
	public static final String SINGLE_SIGN_ON_DISABLED = "service_disabled";
	public static final String TAG = "ZME_Base";
	public static final String SHARED_PREFERENCE_NAME = "zme-sdk-1.0";
	public static final String LOCAL_ACCESSTOKEN_NAME = "zme-at";
	public static final String LOCAL_ACCESSTOKEN_EXPIRES_NAME = "zme-ate";
	public static final String LOCAL_AUTHORIZEDCODE_NAME = "zme-ac";

	public static final int FORCE_DIALOG_AUTH = -1;

	private static final String LOGIN = "oauth/authorize";
	private static final String ACCESS_TOKEN = "oauth/accesstoken";

	public static String DIALOG_BASE_URL;
	public static String GRAPH_BASE_URL;
	
	private String mAuthCode = null;
	private String mAccessToken = null;
	private long mAccessExpires = -99; //not init -99 ; 0 : never expires 

	private String mAppname;
	private String mApiKey;
	private String mSecretKey;
	private ZME_Environment mEnv;
	private boolean mIsDebug = false;

	private ZME_DialogListener mAuthDialogListener;

	private String DEBUG = "";

	/**
	 * Constructor for ZingMe object
	 * 
	 * @param Your ZingMe config
	 *            
	 */
	public ZME_Base(ZME_Config config) {
		
		this.mAppname = config.mAppname;
		this.mApiKey = config.mApiKey;
		this.mSecretKey = config.mSecretKey;
		this.mEnv = config.mEnv;
		this.mIsDebug = config.mIsDebug;
		
		if(this.mAppname == null || TextUtils.isEmpty(this.mAppname)) {
			throw new IllegalArgumentException(
					"You must specify your appname when instantiating a ZingMe object.");
		}
		
		if(this.mApiKey == null || TextUtils.isEmpty(this.mApiKey)) {
			throw new IllegalArgumentException(
					"You must specify your mApiKey when instantiating a ZingMe object.");
		}
		
		if(this.mSecretKey == null || TextUtils.isEmpty(this.mSecretKey)) {
			throw new IllegalArgumentException(
					"You must specify your mSecretKey when instantiating a ZingMe object.");
		}		

		switch (this.mEnv) {
		case DEVELOPMENT:
			ZME_Base.DIALOG_BASE_URL = "https://dev-oauth-me.zing.vn/";
			ZME_Base.GRAPH_BASE_URL = "https://dev-graphapi-me.zing.vn/";
			break;
		case PRODUCTION:
			ZME_Base.DIALOG_BASE_URL = "https://oauth-me.zing.vn/";
			ZME_Base.GRAPH_BASE_URL = "https://graphapi-me.zing.vn/";
			break;
		default: // by default, sdk will use "development" environment
			ZME_Base.DIALOG_BASE_URL = "https://dev-oauth-me.zing.vn/";
			ZME_Base.GRAPH_BASE_URL = "https://dev-graphapi-me.zing.vn/";
			break;
		}		
		
	}	
	

	/**
	 * Retrieve the OAuth 2.0 authorization code after user logged in
	 * successfully.
	 * 
	 * @return String - authorization code
	 */
	public final String getAuthorizationCode() {
		return this.mAuthCode;
	}

	/**
	 * Retrieve the OAuth 2.0 access token for API access: treat with care.
	 * Returns null if no session exists.
	 * 
	 * @return String - access token
	 */
	public final String getAccessToken() {		
		return this.mAccessToken;
	}

	/**
	 * Retrieve the current session's expiration time (in milliseconds since
	 * Unix epoch), or 0 if the session doesn't expire or doesn't exist.
	 * 
	 * @return long - session expiration time
	 */
	public final long getAccessExpires() {
		return this.mAccessExpires;
	}
	
	/**
	 * Set the OAuth 2.0 access token for API access.
	 * 
	 * @param token
	 *            - access token
	 */
	public final void setAccessToken(Context ctx, String token) {
		this.mAccessToken = token;
		this.setAccessTokenFromLocal(ctx, token);
	}

	/**
	 * Set OAuth 2.0 authorization for retrieving access_token later
	 * 
	 * @param code
	 *            authorization code
	 */
	public final void setAuthorizationCode(Context ctx, String code) {		 
		this.mAuthCode = code;
		this.setAuthorizedCodeFromLocal(ctx, code);
	}

	/**
	 * Set the current session's expiration time (in milliseconds since Unix
	 * epoch), or 0 if the session doesn't expire.
	 * 
	 * @param time
	 *            - timestamp in milliseconds
	 */
	public final void setAccessExpires(Context ctx, long time) {
		this.mAccessExpires = time;
		this.setAccessTokenExpiresFromLocal(ctx, time);
	}

	/**
	 * Set the current session's duration (in seconds since Unix epoch).
	 * 
	 * @param expiresIn
	 *            - duration in seconds
	 */
	public final void setAccessExpiresIn(Context ctx, Long expiresIn) {
		if (expiresIn != 0 && expiresIn != -1) {
			setAccessExpires(ctx, System.currentTimeMillis()
					+ expiresIn * 1000);
		}
		else {
			setAccessExpires(ctx, -1); //never expire
		}
	}

	/**
	 * Default authorize method. Grants only basic permissions.
	 * 
	 * See authorize() below for @params.
	 */
	public void authorize(final Context ctx, String state,
			final ZME_DialogListener listener) {
		//get from accessToken, accessTokenExpire, authorizedCode
		if(this.mAccessExpires == -99) { //not init from local
			this.mAccessToken = this.getAccessTokenFromLocal(ctx);
			this.mAccessExpires = this.getAccessTokenExpiresFromLocal(ctx);
			this.mAuthCode = this.getAuthorizedCodeFromLocal(ctx);
		}		
		if(!this.isSessionValid())
			authorize(ctx, new String[] {}, state, listener);
		else {
			Bundle values = new Bundle();
			if (state != null && !state.equals("")) {
				values.putString("state", state);
			}
			listener.onComplete(values);
		}
	}

	/**
	 * Full authorize method
	 * 
	 * Starts a dialog which prompts the user to log in to ZingMe This method
	 * will use OAuth 2.0 for Android to obtain access token
	 * 
	 * @param activity
	 *            The Android activity in which we want to display the
	 *            authorization dialog.
	 * @param permissions
	 *            A list of permissions required for this application: e.g.
	 *            "read_stream", "publish_stream", "offline_access"
	 * @param listener
	 *            Callback interface for notifying the calling application when
	 *            the authentication dialog has completed, failed, or been
	 *            canceled.
	 */
	public void authorize(Context ctx, String[] permissions,
			String state, final ZME_DialogListener listener) {
				
		this.mAuthDialogListener = listener;
		String authorizationCode = this.mAuthCode;
		Log.d(TAG, "authorize code: " + authorizationCode);
		if (authorizationCode == null || authorizationCode.equals("")) {
			//case : don't have authorizeCode
			startDialogAuth(ctx, permissions, state);
		} else {
			try {
				getAccessTokenFromCode(ctx, authorizationCode, state);
//				Bundle values = new Bundle();
//				values.putString("DEBUG", DEBUG);
//				this.mAuthDialogListener.onComplete(values);
			} catch (ZME_Error e) {
				if (e.getErrorCode() == ZME_Error.ZOAUTH_ERR_AUTHORIZED_CODE_INVALID 
						|| e.getErrorCode() == ZME_Error.ZOAUTH_ERR_AUTHORIZED_CODE_EXPIRED
						|| e.getErrorCode() == ZME_Error.ZOAUTH_ERR_USER_HAD_LOGGED_OUT) {
					//expire authorized code => clean
					this.logout(ctx);
					
					startDialogAuth(ctx, permissions, state);
				} else {
					this.mAuthDialogListener.onZmeError(e);
				}
			}
		}
	}

	/**
	 * Internal method to handle dialog-based authentication backend for
	 * authorize().
	 * 
	 * @param activity
	 *            The Android Activity that will parent the auth dialog.
	 * @param permissions
	 *            A list of permissions required for this application. If you do
	 *            not require any permissions, pass an empty String array.
	 */
	private void startDialogAuth(final Context ctx, String[] permissions,
			final String state) {
		Bundle params = new Bundle();
		if (permissions.length > 0) {
			params.putString("scope", TextUtils.join(",", permissions));
		}

		// attach state parameter to prevent CSFR
		if (state != null && !state.equals("")) {
			params.putString("state", state);
		}
		//

		dialog(ctx, LOGIN, params, new ZME_DialogListener() {

			public void onComplete(Bundle values) {
				// ensure any cookies set by the dialog are saved

				//
				String authCode = values.getString(AUTH_CODE);
				if(authCode != null) authCode = URLDecoder.decode(authCode);
				setAuthorizationCode(ctx, authCode);
				
				Log.d(TAG, "authorization code : " + mAuthCode);

				try {
					getAccessTokenFromCode(ctx, authCode, state);										
				} catch (ZME_Error e) {
					mAuthDialogListener.onZmeError(e);
				}

			}

			public void onError(ZME_DialogError error) {
				Log.d(TAG, "Login failed onError: " + error);
				mAuthDialogListener.onError(error);
			}

			public void onZmeError(ZME_Error error) {
				Log.d(TAG, "Login failed onZmeError: "
						+ error.getStackTrace().toString());
				mAuthDialogListener.onZmeError(error);
			}

			public void onCancel() {
				Log.d(TAG, "Login canceled");
				mAuthDialogListener.onCancel();
			}
		});
	}

	/**
	 * @return boolean - whether this object has an non-expired session token
	 */
	public boolean isSessionValid() {
		if(this.mIsDebug) {
			Log.d(TAG, ">>> ZME_Base : isSessionValid => at=" + getAccessToken() + "|at_expires=" + getAccessExpires() + "|now=" + System.currentTimeMillis());
		}
		return (getAccessToken() != null) &&
	                ((getAccessExpires() == 0) ||
	                        (System.currentTimeMillis() < getAccessExpires()));
	}

	/**
	 * Generate a UI dialog for the request action in the given Android context.
	 * 
	 * Note that this method is asynchronous and the callback will be invoked in
	 * the original calling thread (not in a background thread).
	 * 
	 * @param context
	 *            The Android context in which we will generate this dialog.
	 * @param action
	 *            String representation of the desired method: e.g. "login",
	 *            "stream.publish", ...
	 * @param listener
	 *            Callback interface to notify the application when the dialog
	 *            has completed.
	 */
	public void dialog(Context context, String action,
			ZME_DialogListener listener) {
		dialog(context, action, new Bundle(), listener);
	}

	/**
	 * Generate a UI dialog for the request action in the given Android context
	 * with the provided parameters.
	 * 
	 * Note that this method is asynchronous and the callback will be invoked in
	 * the original calling thread (not in a background thread).
	 * 
	 * @param context
	 *            The Android context in which we will generate this dialog.
	 * @param action
	 *            String representation of the desired method: e.g. "feed" ...
	 * @param parameters
	 *            String key-value pairs to be passed as URL parameters.
	 * @param listener
	 *            Callback interface to notify the application when the dialog
	 *            has completed.
	 */
	public void dialog(Context context, String action, Bundle parameters,
			final ZME_DialogListener listener) {

		String url = buildAuthorizationUrl(parameters, action);

		Log.d(TAG, "request URL for ZME_Base: " + url);

		if (context.checkCallingOrSelfPermission(Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED) {
			ZME_Utils.showAlert(context, "Error",
					"Application requires permission to access the Internet");
		} else {
			new ZME_Dialog(context, url, listener).show();
		}
	}

	/**
	 * IMPORTANT: This method must be invoked at the top of the calling
	 * activity's onActivityResult() function or ZingMe authentication will not
	 * function properly!
	 * 
	 * If your calling activity does not currently implement onActivityResult(),
	 * you must implement it and include a call to this method if you intend to
	 * use the authorize() method in this SDK.
	 * 
	 */
	public void authorizeCallback(int requestCode, int resultCode, Intent data) {
		if (resultCode == Activity.RESULT_CANCELED) {
			if (data != null) {
				Log.d(TAG, "User press back button");
				mAuthDialogListener.onCancel();
			} else {
				Log.d(TAG, "Login canceled by user");
				mAuthDialogListener.onCancel();
			}
		} else if (requestCode == Activity.RESULT_OK) {
			Integer errorCode = 0;

			try {
				errorCode = Integer.parseInt(data.getStringExtra("error_code"));
			} catch (NumberFormatException e) {
				Log.e(TAG, "Zing Me returned incorrect data format");
				mAuthDialogListener.onZmeError(new ZME_Error(
						"Failed to receive access token."));
			}

			if (errorCode == 0) {
				if (isSessionValid()) {
					Log.d(TAG, "Log in success!");
					mAuthDialogListener.onComplete(data.getExtras());
				} else {
					Log.d(TAG, "authorizeCallback has error" + errorCode);
					mAuthDialogListener.onZmeError(new ZME_Error(
							"Failed to receive access token."));
				}
			} else {
				Log.d(TAG, "Log in failed!");
				String errorType = data.getStringExtra("error_type");
				String errorMsg = data.getStringExtra("error_message");
				mAuthDialogListener.onZmeError(new ZME_Error(errorMsg,
						errorType, errorCode));
			}
		}
	}

	public void logout(Context ctx) {		
		ZME_Utils.clearCookies(ctx);
		setAccessToken(ctx, null);
		
		setAccessExpires(ctx, -2);
				
		setAuthorizationCode(ctx, null);		
	}

	public void getAccessTokenFromCode(final Context ctx, String code, final String state) throws ZME_Error {
		
		mAuthCode = code;
				
		new Thread() {
			@Override
			public void run() {
				String endpoint = DIALOG_BASE_URL + ACCESS_TOKEN;
				HashMap<String, String> params = new HashMap<String, String>();
				params.put("client_id", mApiKey);
				params.put("client_secret", mSecretKey);
				params.put("code", mAuthCode);

				JSONObject data;
				try {
					DEBUG += "AC: " + mAuthCode + "\n";
					DEBUG += "start to get AT from AC at " + new Date() + "\n";
					data = (JSONObject)ZME_Connection.request(endpoint, params);
					Log.d(TAG, "getAccessTokenFromCode - data: " + data.toString());
					if(data != null) {
						setAccessToken(ctx, data.getString(TOKEN));
						DEBUG += "AT got from server: " + mAccessToken + "at "
								+ new Date() + "\n";
						
						setAccessExpiresIn(ctx, data.getLong(EXPIRES));
					}
					Bundle values = new Bundle();
					if (state != null && !state.equals("")) {
						values.putString("state", state);
					}
					mAuthDialogListener.onComplete(values);
					
				} catch (JSONException e) {
					mAuthCode = "";
					//throw new ZME_Error("JSON decode error", -10001);
					mAuthDialogListener.onZmeError(new ZME_Error("JSON decode error", -10001));
				} catch (ZME_Error e) {
					mAuthCode = "";
//					throw e;
					mAuthDialogListener.onZmeError(e);
					
				} catch (Exception e) {
					mAuthCode = "";
					Log.d(TAG, "Has exception", e);
//					throw new ZME_Error("Cannot get access token from Code");
					mAuthDialogListener.onZmeError(new ZME_Error("Cannot get access token from Code"));
				}
			}
		}.start();
		
		
	}	

	private String buildAuthorizationUrl(Bundle parameters, String action) {
		try {
			String endpoint = DIALOG_BASE_URL + action;
			parameters.putString("display", "touch");

			if (action.equals(LOGIN)) {
				parameters.putString("type", "user_agent");
				parameters.putString("client_id", this.mApiKey);
			}

			String url = endpoint + "?" + ZME_Utils.encodeUrl(parameters);
			return url;
		} catch (Exception ex) {
			Log.e(TAG, "buildAuthorizationUrl: " + ex.getMessage());
			return "";
		}
	}
	
	/////////////////////// get authorized code + accesstoken + accessTokenExpires from local storage
	private String getAccessTokenFromLocal(final Context ctx) {
		String at = ctx.getSharedPreferences(SHARED_PREFERENCE_NAME,
				Context.MODE_PRIVATE).getString(LOCAL_ACCESSTOKEN_NAME, null);
		return at;
	}
	
	private void setAccessTokenFromLocal(final Context ctx, String accessToken) {
		Editor editor = ctx.getSharedPreferences(SHARED_PREFERENCE_NAME,
				Context.MODE_PRIVATE).edit();
		editor.putString(LOCAL_ACCESSTOKEN_NAME, accessToken);
		editor.commit();
	}
	
	private long getAccessTokenExpiresFromLocal(final Context ctx) {
		
		long expires = ctx.getSharedPreferences(SHARED_PREFERENCE_NAME,
				Context.MODE_PRIVATE).getLong(LOCAL_ACCESSTOKEN_EXPIRES_NAME, -1);
		return expires;
	}
	
	private void setAccessTokenExpiresFromLocal(final Context ctx, long expires) {
		Editor editor = ctx.getSharedPreferences(SHARED_PREFERENCE_NAME,
				Context.MODE_PRIVATE).edit();
		editor.putLong(LOCAL_ACCESSTOKEN_EXPIRES_NAME, expires);
		editor.commit();
	}	
	
	private String getAuthorizedCodeFromLocal(final Context ctx) {
		String ac = ctx.getSharedPreferences(SHARED_PREFERENCE_NAME,
				Context.MODE_PRIVATE).getString(LOCAL_AUTHORIZEDCODE_NAME, null);
		return ac;
	}
	
	private void setAuthorizedCodeFromLocal(final Context ctx, String authorizedCode) {
		Editor editor = ctx.getSharedPreferences(SHARED_PREFERENCE_NAME,
				Context.MODE_PRIVATE).edit();
		editor.putString(LOCAL_AUTHORIZEDCODE_NAME, authorizedCode);
		editor.commit();
	}
		
	private void dumpDebug(String log) {
		if(this.mIsDebug) {
			Log.d(TAG, log);
		}
	}
		
	
	
}
